Spring Data JDBC 란?
도메인 중심 설계 원칙에 따라 JDBC 데이터베이스를 사용하는 솔루션 개발에 핵심 Spring 개념을 적용한 것이다.
JDBC Aggregate 의 핵심 기능은 Spring 컨테이너의 IoC 서비스를 호출할 필요 없이 직접 사용할 수 있다. 이는 Spring 컨테이너의 다른 서비스 없이 "독립적으로 사용할 수 있는" JdbcTemplate
과 매우 유사하다.
Requirements
JDK 8.0 이상, Spring Framework 6.0.13 이상이 필요하다.
DB2, H2, HSQLDB, MariaDB, MySQL, Oracle, Postgres, Microsoft SQL Server 를 직접 지원한다.
다음과 같은 기본 기능들을 제공한다.
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity); (1)
Optional<T> findById(ID primaryKey); (2)
Iterable<T> findAll(); (3)
long count(); (4)
void delete(T entity); (5)
boolean existsById(ID primaryKey); (6)
// … more functionality omitted.
}
ListCrudRepository
는 이와 동등한 메서드를 제공하지만, CrudRepository
메서드가 Iterable
을 반환하는 것과 달리 List
를 반환한다.
- [i]
JpaRepository
나MongoRepository
와 같이 persistence 기술 별 추상화도 제공한다. 이런 인터페이스는CrudRepository
와 같은 다소 일반적인 persistence 기술에 구애받지 않는 인터페이스에 더해,CrudRepository
를 확장하고 기본 persistence 기술의 기능을 노출한다.
CrudRepository
에 더해서, 엔티티에 대한 페이징 접근을 용이하게 하는 메서드를 추가하는 PagingAndSortingRepository
도 제공한다.
public interface PagingAndSortingRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
다음과 같이 사용할 수 있다.
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));
이 외에도 count 쿼리, delete 쿼리에 대한 쿼리 파생이 가능하다.
interface UserRepository extends CrudRepository<User, Long> {
long countByLastname(String lastname);
}
interface UserRepository extends CrudRepository<User, Long> {
long deleteByLastname(String lastname);
List<User> removeByLastname(String lastname);
}
쿼리 메서드
다음과 같은 레포지터리가 있다고 가정한다.
interface PersonRepository extends Repository<Person, Long> { … }
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLastname(String lastname);
}
JavaConfig
나 XML Configuration
으로 해당 인터페이스에 대한 프록시 인스턴스를 스프링에 설정한다.
import org.springframework.data.….repository.config.EnableJpaRepositories;
@EnableJpaRepositories
class Config { … }
그 뒤, 레포지터리를 주입 받아 사용한다.
class SomeClient {
private final PersonRepository repository;
SomeClient(PersonRepository repository) {
this.repository = repository;
}
void doSomething() {
List<Person> persons = repository.findByLastname("Matthews");
}
}
쿼리 생성하기
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
JPA 와 유사하다.
distinct
, ignoreCase
, order by
를 설정할 수 있다.
주의할 점
- 표현식은 일반적으로 연결될 수 있는 (concatenated) operator 로 결합된 property 순회 (traversal) 이다.
- property expression 에 대해
Between
,LessThan
,GreaterThan
,Like
와 같은 연산자도 지원된다. datastore 마다 다를 수 있다.
- property expression 에 대해
- 메서드 구문 분석기는 개별 속성 또는 대/소문자 ignore 를 지원하는 모든 속성에 대한
ignoreCase
플래그 설정이 지원된다. 대/소문자 ignore 지원 여부는 store 마다 다를 수 있다. - 속성을 참조하는 쿼리 메서드에
OrderBy
절을 추가하고 정렬 방향 (Asc
,Desc
)를 지정하여 순서를 적용할 수 있다. 자세한 내용은 Paging, Iterating Large Results, Sorting 을 참고하자.
속성 표현식
관리되는 엔티티의 직접 속성만 참조할 수 있다. 쿼리 생성 시 구문 분석된 property 가 관리되는 도메인 클래스의 property 인지 확인하게 된다. 그러나 중첩된 속성을 traverse 하여 제약 조건을 정의할 수도 있다.
다음과 같은 예시를 보자.
List<Person> findByAddressZipCode(ZipCode zipCode);
Person
클래스에 ZipCode
를 갖는 Address
가 있다고 가정한다. 이 경우, 메서드는 x.address.zipCode
속성 traverse 를 생성한다. resolution algorithm 이 AddressZipCode
를 property 로 해석하는 것부터 시작해서, 도메인 클래스에서 해당 이름의 property 가 존재하는지 부터 확인한다. 알고리즘이 성공하면 (해당 property 이름이 도메인 클래스에 존재하면) 해당 property 를 사용한다.
그렇지 않은 경우에는 알고리즘이 오른쪽에서 camel-case 부분의 소스를 head, tail 로 분할하여 해당 속성을 찾으려고 시도한다. (addressZip
, Code
로 분리) head 부분의 property 를 찾으면 동일 알고리즘을 다시 실행한다. 해당 분할이 일치하지 않는 경우, 분할 지점을 address
, ZipCode
로 이동하여 계속 진행한다.
대부분의 경우 해당 방법이 작동하지만, 알고리즘이 잘못된 속성을 선택할 수 있다. Person
클래스에 addressZip
속성도 있다고 가정하면, 아마도 알고리즘은 첫 번째 분할 라운드에서 잘못된 속성을 선택한 뒤 실패할 것이다.
이런 모호함을 해결하려면, 다음과 같이 메서드 이름 안에 _
를 사용하여 traverse point 를 수동으로 정의할 수 있다.
List<Person> findByAddress_ZipCode(ZipCode zipCode);
Paging, Iterating Large Results, Sorting
쿼리에서 매개변수를 처리하려면, 앞의 예제에서 이미 본 것처럼 메서드 매개변수를 정의하면 된다. 그 외에도 Pageable
, Sort
와 같은 특정 유형을 인식하여 쿼리에 페이지네이션, 정렬을 동적으로 적용할 수 있다.
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
Sort, Pageable 을 사용하는 API 는 메서드에 null 이 아닌 값을 전달할 것으로 예상한다. 만약 Sort, Pageable 을 사용하지 않으려면, Sort.unsorted() 나 Pageable.unpaged() 를 사용해야 한다.
자세한 설명은 어떤 메서드가 적합할까? 를 확인하는 것이 좋다.
Aggregate Roots 에서 이벤트 발행하기
Publishing Events from Aggregate Roots 참고
레포지터리에서 관리하는 엔티티는 Aggregate Root 이다. 도메인 중심 디자인 애플리케이션에서 이런 Aggregate Root 는 일반적으로 도메인 이벤트를 발행한다. Spring Data 는 @DomainEvents
라는 어노테이션을 제공한다.
class AnAggregateRoot {
@DomainEvents (1)
Collection<Object> domainEvents() {
// … return events you want to get published here
}
@AfterDomainEventPublication (2)
void callbackMethod() {
// … potentially clean up domain events list
}
}
@DomainsEvents
를 사용하는 메서드는 단일 이벤트 인스턴스 또는 이벤트 컬렉션을 반환할 수 있다. 인수를 받지 않아야 한다.
모든 이벤트가 발행된 후에는 @AfterDomainEventPublication
으로 어노테이션이 달린 메서드를 사용할 수 있다. 이 메서드로 발행할 이벤트 목록들을 잠재적으로(potentially) 정리(clean)할 수 있다.
이 메서드들은 다음 중 하나의 Spring Data Repository 메서드가 호출될 때마다 호출된다.
- save, saveAll
- delete, deleteAll, deleteAllInBatch, deleteInBatch
해당 메서드들은 Aggregate Root 인스턴스들을 인자로 받는다. deleteById 는 인스턴스를 삭제하는 쿼리를 실행하도록 선택할 수 있기 때문에 애초에 Aggregate 인스턴스에 접근할 수 없어 제외되었다.
특성
Core concepts
Spring Data Repository abstraction 의 중심 인터페이스는 Repository
이다.
CrudRepository
나 ListCrudRepository
인터페이스는 관리 중인 엔티티 클래스에 대한 정교한 CRUD 기능을 제공한다.
시작하기
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
build.gradle
파일에 필요한 의존성을 추가한다.
스키마 설정
Spring Data JDBC 는 자동으로 데이터베이스 스키마를 생성하지 않는다. 따라서 스키마를 수동으로 생성해야 한다. 여기서는 schema.sql
을 사용한 방법으로 진행해보겠다.
Repository 생성
public interface MenuRepository extends CrudRepository<Menu, Long> {
}